import json

from fastapi import FastAPI, Request
from fastapi.exceptions import HTTPException, RequestValidationError
from fastapi.responses import JSONResponse
from app.models import responses
from app.database.redis import get_backend_nonce_redis, get_backend_dfp_redis, get_backend_ip_redis
from app.utils.time import get_now_date
from app.utils.ip import get_real_ip
from app.utils import FM
from app.config import get_settings


# 定义中间件
async def fixPathMiddleware(request: Request, call_next):
    """
    中间件：修复路径端点
    行为 1: 移除路径长度大于 1 的末端的反斜杠, 其他情况保持不变

    例如：/api/v1/login/ 这种情况将被调整为 /api/v1/login
    """
    raw_path = request.scope.get('path', '')
    if len(raw_path) > 1 and raw_path.endswith('/'):
        path = raw_path[:-1]
    else:
        path = raw_path
    request.scope['path'] = path
    response = await call_next(request)
    return response


prefix = 'FETCH-FM'.lower()
excludes = [
    '/index/trace', '/files/upload'
]


def check_valid_path(path: str):
    for exclude in excludes:
        if path.find(exclude) == -1:
            return False

    return True


def validate_timestamp(timestamp: int):
    now = int(get_now_date().timestamp())
    return now - 60 <= timestamp <= now + 60


async def validateFMLevel1Middleware(request: Request, call_next):
    """
    FM 基础校验

    1. 检查请求头完整性（FETCH-FM-TS/FETCH-FM-NONCE/FETCH-FM-SIGN/FETCH-FM-DFP必须存在）
    2. 时间戳校验：服务器时间与 FETCH-FM-TS 差异需在 ±1 分钟内
    3. Nonce 校验：检查 Redis 中是否存在该 nonce（防重放攻击）
    """
    path: str = request.scope.get('path', '')
    if check_valid_path(path):
        headers = request.headers
        # 检查请求头完整性
        if not all([
            headers.get(f'{prefix}-ts'),
            headers.get(f'{prefix}-nonce'),
            headers.get(f'{prefix}-sign'),
            headers.get(f'{prefix}-dfp')
        ]):
            return JSONResponse(
                content=responses.Response101().model_dump(),
                status_code=200
            )

        # 时间戳校验
        if not validate_timestamp(int(headers.get(f'{prefix}-ts', 0))):
            return JSONResponse(
                content=responses.Response101().model_dump(),
                status_code=200
            )

        # Nonce 校验
        nonce_redis = get_backend_nonce_redis()
        nonce = headers.get(f'{prefix}-nonce', '')
        if not nonce or await nonce_redis.exists(nonce):
            return JSONResponse(
                content=responses.Response101().model_dump(),
                status_code=200
            )
        else:
            await nonce_redis.set(nonce, 1, ex=60)

    response = await call_next(request)
    return response


async def validateFMLevel2Middleware(request: Request, call_next):
    """
    FM 动态签名校验

    1. 签名校验
    2. DFP 统计
    3. IP 统计
    """
    path: str = request.scope.get('path', '')
    if check_valid_path(path):
        ip = get_real_ip(request)
        client_sign = request.headers.get(f'{prefix}-sign', '')
        query_params = dict(request.query_params)
        timestamp = request.headers.get(f'{prefix}-ts', '0')
        nonce = request.headers.get(f'{prefix}-nonce', '')
        dfp = request.headers.get(f'{prefix}-dfp', '')
        server_sign = FM.generate_signature(
            request.method, request.scope.get('path', ''),
            query_params, timestamp, nonce, dfp
        )
        if not FM.compare_signature(client_sign, server_sign):
            # DFP 统计
            dfp_ex_seconds = 60 * 60 * 24 * get_settings().DFP_EXPIRE_DAYS
            dfp_redis = get_backend_dfp_redis()
            count = await dfp_redis.get(f'count:{dfp}')
            if not count:
                await dfp_redis.set(f'count:{dfp}', 1, ex=60)
            else:
                if int(count) < 5:
                    await dfp_redis.set(f'count:{dfp}', int(count) + 1, ex=60)
                else:
                    await dfp_redis.set(f'black:{dfp}', 1, ex=dfp_ex_seconds)

            # IP 统计
            ip_ex_seconds = 60 * 60 * 24 * get_settings().IP_EXPIRE_DAYS
            ip_redis = get_backend_ip_redis()
            count = await ip_redis.get(f'count:{ip}')
            if not count:
                await ip_redis.set(f'count:{ip}', 1, ex=60)
            else:
                if int(count) < 30:
                    await ip_redis.set(f'count:{ip}', int(count) + 1, ex=60)
                else:
                    await ip_redis.set(f'black:{ip}', 1, ex=ip_ex_seconds)

            return JSONResponse(
                content=responses.Response101().model_dump(),
                status_code=200
            )

    return await call_next(request)


async def validateFMLevel3Middleware(request: Request, call_next):
    """
    FM 深层校验

    1. 指纹 IP 检查
    2. 请求体内容校验
    3. 删除DFP、IP
    """
    path: str = request.scope.get('path', '')
    if check_valid_path(path):
        dfp = request.headers.get(f'{prefix}-dfp', '')
        dfp_model = FM.parse_dfp(dfp)
        if dfp_model.check_risk():
            return JSONResponse(
                content=responses.Response101().model_dump(),
                status_code=200
            )

        dfp_redis = get_backend_dfp_redis()
        if await dfp_redis.exists(f'black:{dfp}'):
            return JSONResponse(
                content=responses.Response101().model_dump(),
                status_code=200
            )

        ip = get_real_ip(request)
        ip_redis = get_backend_ip_redis()
        if await ip_redis.exists(f'black:{ip}'):
            return JSONResponse(
                content=responses.Response101().model_dump(),
                status_code=200
            )

        try:
            data = await request.json()
        except json.JSONDecodeError:
            data = {}
        key = request.headers.get(f'{prefix}-nonce', '')
        client_chash = request.headers.get(f'{prefix}-chash', '')
        server_chash = FM.generate_chash(data, key)
        if not FM.compare_chash(client_chash, server_chash):
            return JSONResponse(
                content=responses.Response101().model_dump(),
                status_code=200
            )

        if await dfp_redis.exists(f'count:{dfp}'):
            await dfp_redis.delete(f'count:{dfp}')

        if await ip_redis.exists(f'count:{ip}'):
            await ip_redis.delete(f'count:{ip}')

    response = await call_next(request)
    return response


middlewares = [
    fixPathMiddleware,
    validateFMLevel1Middleware,
    validateFMLevel2Middleware,
    validateFMLevel3Middleware
]
"""中间件列表"""


def register_middlewares(app: FastAPI):
    """注册中间件 """
    for middleware in middlewares:
        app.middleware('http')(middleware)

    return len(middlewares)


def register_exc_handler(app: FastAPI):
    @app.exception_handler(405)
    async def handle_405(_request: Request, _exc: HTTPException):
        return JSONResponse(
            content=responses.Response103().model_dump(),
            status_code=200
        )

    @app.exception_handler(RequestValidationError)
    async def handle_RequestValidationError(_request: Request, exc: RequestValidationError):
        if app.settings.is_dev:
            data = {
                'error': exc.errors()
            }
        else:
            data = None
        return JSONResponse(
            content=responses.Response102(data=data).model_dump(),
            status_code=200
        )

    @app.exception_handler(500)
    async def handle_500(_request: Request, exc: Exception):
        if app.settings.is_dev:
            data = {
                'error': str(exc)
            }
        else:
            data = None
        return JSONResponse(
            content=responses.Response105(data=data).model_dump(),
            status_code=200
        )

    @app.exception_handler(404)
    async def handle_404(_request: Request, _exc: Exception):
        return JSONResponse(
            content=responses.Response104().model_dump(),
            status_code=200
        )

    @app.exception_handler(responses.FailureResponseError)
    async def handle_FailureResponseError(_request: Request, exc: responses.FailureResponseError):
        return JSONResponse(
            content=exc.response().model_dump(),
            status_code=200
        )
